////////////////////////////////////////////////////////////////////////
//
//     Copyright (c) 2009-2013 Denim Group, Ltd.
//
//     The contents of this file are subject to the Mozilla Public License
//     Version 2.0 (the "License"); you may not use this file except in
//     compliance with the License. You may obtain a copy of the License at
//     http://www.mozilla.org/MPL/
//
//     Software distributed under the License is distributed on an "AS IS"
//     basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
//     License for the specific language governing rights and limitations
//     under the License.
//
//     The Original Code is ThreadFix.
//
//     The Initial Developer of the Original Code is Denim Group, Ltd.
//     Portions created by Denim Group, Ltd. are Copyright (C)
//     Denim Group, Ltd. All Rights Reserved.
//
//     Contributor(s): Denim Group, Ltd.
//
////////////////////////////////////////////////////////////////////////
package com.denimgroup.threadfix.data.dao.hibernate;

import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.denimgroup.threadfix.data.dao.VulnerabilityDao;
import com.denimgroup.threadfix.data.entities.Application;
import com.denimgroup.threadfix.data.entities.DeletedVulnerability;
import com.denimgroup.threadfix.data.entities.Finding;
import com.denimgroup.threadfix.data.entities.Vulnerability;

/**
 * Hibernate Vulnerability DAO implementation. Most basic methods are
 * implemented in the AbstractGenericDao
 * 
 * @author mcollins, dwolf
 * @see AbstractGenericDao
 */
@Repository
public class HibernateVulnerabilityDao implements VulnerabilityDao {

	private SessionFactory sessionFactory;

	@Autowired
	public HibernateVulnerabilityDao(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

	@Override
	@SuppressWarnings("unchecked")
	public List<Vulnerability> retrieveAll() {
		return sessionFactory
				.getCurrentSession()
				.createQuery(
						"from Vulnerability vulnerability "
								+ "where vulnerability.expired = :false order by vulnerability.id ")
				.setBoolean("false", false).list();
	}

	@Override
	@SuppressWarnings("unchecked")
	public List<Vulnerability> retrieveAllActive() {
		return sessionFactory
				.getCurrentSession()
				.createQuery(
						"from Vulnerability vulnerability "
								+ "where vulnerability.active = :true "
								+ "and vulnerability.expired = :false")
				.setBoolean("false", false).setBoolean("true", true).list();
	}

	@Override
	@SuppressWarnings("unchecked")
	public List<Vulnerability> retrieveAllActiveByApplication(int applicationId) {
		return sessionFactory
				.getCurrentSession()
				.createQuery(
						"from Vulnerability vuln where vuln.application = :appId "
								+ "and vuln.active = :true and vuln.expired = :false")
				.setInteger("appId", applicationId).setBoolean("true", true)
				.setBoolean("false", false).list();
	}

	@Override
	@SuppressWarnings("unchecked")
	public List<Vulnerability> retrieveAllByGenericVulnerabilityAndApp(
			Vulnerability vulnerability) {
		return sessionFactory
				.getCurrentSession()
				.createQuery(
						"from Vulnerability vuln where vuln.application = :appId "
								+ "and vuln.genericVulnerability = :gvId and vuln.expired = :false")
				.setInteger("gvId",
						vulnerability.getGenericVulnerability().getId())
				.setInteger("appId", vulnerability.getApplication().getId())
				.setBoolean("false", false).list();
	}

	@Override
	@SuppressWarnings("unchecked")
	public List<Vulnerability> retrieveAllInactive() {
		return sessionFactory
				.getCurrentSession()
				.createQuery(
						"from Vulnerability vulnerability "
								+ "where vulnerability.active = :false "
								+ "and vulnerability.expired = :false")
				.setBoolean("false", false).list();
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<Vulnerability> retrieveByApplicationIdList(List<Integer> applicationIdList)  {
			return sessionFactory.getCurrentSession()
				.createQuery("from Vulnerability vulnerability " +
						"where vulnerability.application.id in (:idList)")
						.setParameterList("idList", applicationIdList).list();
		}

	@Override
	public Vulnerability retrieveByHashAndApp(String hash, int applicationId) {
		// the function defaults to locationVulnerabilityHash (to avoid a super
		// long name)
		return (Vulnerability) sessionFactory
				.getCurrentSession()
				.createQuery(
						"from Vulnerability vulnerability "
								+ "where vulnerability.locationVariableHash = :hash "
								+ "and vulnerability.application = :appId "
								+ "and vulnerability.expired = :false")
				.setString("hash", hash).setInteger("appId", applicationId)
				.setBoolean("false", false).uniqueResult();
	}

	@Override
	public Vulnerability retrieveById(int id) {
		Vulnerability vuln = (Vulnerability) sessionFactory.getCurrentSession()
				.get(Vulnerability.class, id);
		if (vuln != null && !vuln.isExpired()) {
			return vuln;
		} else {
			return null;
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<Vulnerability> retrieveByLocationHashAndApp(String hash,
			int applicationId) {
		return sessionFactory
				.getCurrentSession()
				.createQuery(
						"from Vulnerability vulnerability "
								+ "where vulnerability.locationHash = :hash "
								+ "and vulnerability.application = :appId "
								+ "and vulnerability.expired = :false")
				.setString("hash", hash).setInteger("appId", applicationId)
				.setBoolean("false", false).list();
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<Vulnerability> retrieveByVariableHashAndApp(String hash,
			int applicationId) {
		return sessionFactory
				.getCurrentSession()
				.createQuery(
						"from Vulnerability vulnerability "
								+ "where vulnerability.variableHash = :hash "
								+ "and vulnerability.application = :appId "
								+ "and vulnerability.expired = :false")
				.setString("hash", hash).setInteger("appId", applicationId)
				.setBoolean("false", false).list();
	}

	@Override
	@SuppressWarnings("unchecked")
	public List<Vulnerability> retrieveSimilarHashes(Vulnerability vulnerability) {
		return sessionFactory
				.getCurrentSession()
				.createQuery(
						"from Vulnerability vuln where vuln.application = :appId "
								+ "and vuln.expired = :false "
								+ "and (vuln.locationVariableHash = :lvHash or "
								+ "vuln.locationHash = :lHash or vuln.variableHash = :vHash)")
				.setString("lvHash", vulnerability.getLocationVariableHash())
				.setString("lHash", vulnerability.getLocationHash())
				.setString("vHash", vulnerability.getVariableHash())
				.setInteger("appId", vulnerability.getApplication().getId())
				.setBoolean("false", false).list();
	}

	@Override
	public void saveOrUpdate(Vulnerability vulnerability) {
		sessionFactory.getCurrentSession().saveOrUpdate(vulnerability);
	}

	@Override
	public void delete(Vulnerability vulnerability) {
		sessionFactory.getCurrentSession().save(new DeletedVulnerability(vulnerability));
		sessionFactory.getCurrentSession().delete(vulnerability);
	}
	
	/**
	 * I would feel bad about having so much logic here but the alternatives are 
	 * passing in a query string which is a terrible idea or 
	 * having a ton of methods which also isn't any good.
	 */
	@SuppressWarnings("unchecked")
	@Override
	public List<Vulnerability> retrieveActiveByAppIdAndPage(int appId, int page,  
			int sort, int field, Integer cwe, String description, String severity, 
			String path, String param, boolean open, boolean falsePositive) {

		String[] headers = new String[] { "",
										  "vuln.name", 
										  "severity.intValue", 
										  "surface.path",
										  "surface.parameter" };
										  
		Criteria criteria = sessionFactory.getCurrentSession().createCriteria(Vulnerability.class);
		
		if (!open && falsePositive) {
			criteria.add(Restrictions.eq("isFalsePositive", true));
		} else {
			criteria.add(Restrictions.eq("active", open))
					.add(Restrictions.eq("isFalsePositive", falsePositive));
		}
		
		criteria.add( Restrictions.eq("application.id", appId))
				.createAlias("genericSeverity", "severity")
				.createAlias("genericVulnerability", "vuln")
				.createAlias("surfaceLocation", "surface")
				.setFirstResult((page - 1) * 100)
				.setMaxResults(100);
		
		// Add Filtering restrictions
		if (description != null)
			criteria.add(Restrictions.like("vuln.name", "%" + description + "%").ignoreCase());
		
		if (severity != null)
			criteria.add(Restrictions.like("severity.name", "%" + severity + "%").ignoreCase());
		
		if (path != null)
			criteria.add(Restrictions.like("surface.path", "%" + path + "%").ignoreCase());
		
		if (param != null)
			criteria.add(Restrictions.like("surface.parameter", "%" + param + "%").ignoreCase());
		
		if (cwe != null) {
			criteria.add(Restrictions.eq("vuln.id", cwe));
		}
		
		// Add Ordering
		if ((sort != 1 && sort != 2) ||
					field <= 0 || field > headers.length) {
			criteria.addOrder( Order.desc("severity.intValue") )
					.addOrder( Order.asc("vuln.name") )
					.addOrder( Order.asc("surface.path") )
					.addOrder( Order.asc("surface.parameter") );
		} else {
			String item = headers[field];
			if (sort == 1) {
				criteria.addOrder( Order.asc(item));
			} else if (sort == 2) {
				criteria.addOrder( Order.desc(item));
			}
		}
				
		return criteria.list();
	}
	
	@Override
	public long getVulnCountWithFilters(Integer appId, String description, 
			String severity, String path, String param, Integer cweInteger, 
			boolean open, boolean falsePositive) {

		Criteria criteria = sessionFactory.getCurrentSession().createCriteria(
				Vulnerability.class);

		if (!open && falsePositive) {
			criteria.add(Restrictions.eq("isFalsePositive", true));
		} else {
			criteria.add(Restrictions.eq("active", open))
					.add(Restrictions.eq("isFalsePositive", falsePositive));
		}
	
		criteria.add(Restrictions.eq("application.id", appId))
				.createAlias("genericSeverity", "severity")
				.createAlias("genericVulnerability", "vuln")
				.createAlias("surfaceLocation", "surface");

		// Add Filtering restrictions
		if (description != null)
			criteria.add(Restrictions
					.like("vuln.name", "%" + description + "%").ignoreCase());

		if (severity != null)
			criteria.add(Restrictions.like("severity.name",
					"%" + severity + "%").ignoreCase());

		if (path != null)
			criteria.add(Restrictions.like("surface.path", "%" + path + "%")
					.ignoreCase());
		
		if (cweInteger != null) {
			criteria.add(Restrictions.eq("vuln.id", cweInteger));
		}

		if (param != null)
			criteria.add(Restrictions.like("surface.parameter",
					"%" + param + "%").ignoreCase());

		return (Long) criteria.setProjection(Projections.rowCount()).uniqueResult();
	}
	
	@Override
	public long getVulnCount(Integer appId, boolean open) {
		return (Long) sessionFactory
							.getCurrentSession()
							.createCriteria(Vulnerability.class)
							.add(Restrictions.eq("active", open))
							.add(Restrictions.eq("application.id", appId))
							.setProjection(Projections.rowCount())
							.uniqueResult();
	}
	
	@SuppressWarnings("unchecked")
	public List<Vulnerability> getFalsePositiveVulnCount(Application application,
			boolean value) {
		return sessionFactory
				.getCurrentSession()
				.createQuery("from Vulnerability vuln where vuln.application = :appId "
								+ "and vuln.isFalsePositive = :fp")
				.setBoolean("fp", value)
				.setInteger("appId", application.getId()).list();
	}

	@Override
	public void evict(Finding finding) {
		sessionFactory.getCurrentSession().evict(finding);
	}
}
